# coding: utf-8
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QStackedWidget
from pydantic import BaseModel
import requests # 新增
import json
from qfluentwidgets import ScrollArea,TextEdit, Pivot, FluentIcon as FIF, InfoBar, PrimaryPushSettingCard, OptionsSettingCard, Dialog # 新增 Dialog
# 导入拖放控件
from app.components.custom_drop_file_widget import CustomDropSingleFileWidget, CustomDropMultiFilesWidget
from app.components.line_edit_card import LineEditSettingCard
from app.components.slider_card import SliderSettingCard
from qfluentwidgets import SettingCardGroup
from ..common.style_sheet import StyleSheet
from ..common.config import cfg
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import PydanticOutputParser
from langchain.output_parsers import OutputFixingParser
from langchain_core.prompts import PromptTemplate
from ..common.setting import BASE_URL,ANKI_URL
import io
from PIL import Image
import base64
from langchain_core.messages import HumanMessage

class TaskWorker(QThread): 
    """处理耗时任务的工作线程"""
    # 定义信号
    success = Signal(str)  # 成功信号，传递成功消息
    error = Signal(str)    # 错误信号，传递错误消息
    progress = Signal(str) # 进度信号，传递进度信息
    
    def __init__(self, form_data, ai_card_interface):
        super().__init__()
        self.form_data = form_data
        self.ai_interface = ai_card_interface
        self.is_cancelled = False
    
    def run(self):
        """在后台线程中执行的主要逻辑"""
        try:
            self.progress.emit("开始处理任务...")
            
            # 获取模板字段信息
            template_name = self.form_data.get("template_name", "")
            self.progress.emit("获取模板信息...")
            
            # 使用AnkiConnect API获取模板字段信息
            payload = {
                "action": "modelFieldNames",
                "version": 6,
                "params": {
                    "modelName": template_name
                }
            }
            
            response = requests.post(ANKI_URL, json=payload)
            if response.status_code != 200:
                self.error.emit("无法连接到Anki")
                return
                
            result = response.json()
            if result.get("error") is not None:
                self.error.emit(f"无法获取模板字段: {result.get('error')}")
                return
                
            fields = result.get("result", [])
            
            # 获取字段描述
            field_descriptions = {}
            desc_payload = {
                "action": "modelFieldDescriptions",
                "version": 6,
                "params": {
                    "modelName": template_name
                }
            }
            
            desc_response = requests.post(ANKI_URL, json=desc_payload)
            if desc_response.status_code == 200:
                desc_result = desc_response.json()
                if desc_result.get("error") is None:
                    descriptions = desc_result.get("result", {})
                    field_descriptions = descriptions
            
            # 动态生成Pydantic类定义
            pydantic_class_code = f"""
from pydantic import BaseModel, Field

class Card(BaseModel):
    \"\"\"Anki card fields for {template_name} template.\"\"\"
"""
            
            for i in range(len(field_descriptions)):
                description = field_descriptions[i]
                if not description:
                    continue
                pydantic_class_code += f"    {fields[i].lower().replace(' ', '_')}: str = Field(description=\"{description}\")\n"
            
            card_class = f'''
{pydantic_class_code}
class CardList(BaseModel):
    """Card list to create."""
    cards: list[Card] = Field(description="这是卡片列表")
'''
            
            # 准备LLM和解析器
            self.progress.emit("初始化AI模型...")
            namespace = {}
            exec(card_class, namespace)
            Card = namespace.get('Card')
            CardList = namespace.get('CardList')
            
            llm = ChatOpenAI(
                model=cfg.get(cfg.llmName),
                api_key=cfg.get(cfg.llmApiKey),
                base_url=BASE_URL
            )
            
            parser = PydanticOutputParser(pydantic_object=CardList)
            new_parser = OutputFixingParser.from_llm(parser=parser, llm=llm)
            
            # 构建提示词
            card_count = self.form_data.get("ai_card_count", "0")
            if int(card_count) > 0:
                card_count = f"卡片数量：{card_count}\n\n"
            else:
                card_count = f"卡片数量：数量不限定直至完整覆盖所有知识点不要遗漏\n\n"
                
            custom_prompt = f"""
        你是一个专业的Anki卡片制作助手，请根据用户提供的内容创建高质量的学习卡片。
        {card_count}
        请严格遵守以下规则，以确保卡片效果最佳：
        ## 1. 用户自定义规则 (最高优先级)
        *   {self.form_data.get("custom_prompt","无用户自定义规则")}
        *   **重要**: 若本规则与下方任何规则冲突，**始终以本用户自定义规则为准**。
        ## 2. Anki卡片核心原则 (至关重要)
        *   **知识原子化**: 每张卡片只包含一个独立的知识点，确保内容精炼，减轻记忆负担。
        *   **全面覆盖**: 完整覆盖输入资料中的所有核心知识点，无遗漏。
        *   **价值导向**: 答案内容必须是最具学习价值和核心性的信息。
        *   **问题唯一性**: 确保生成的每个问题都是独一无二的，避免重复。
        *   **数学公式 (MathJax)**:
            *   格式: 行内公式 `\(公式\)`，块级公式 `\[公式\]`。请根据公式长度选择使用行内公式还是块级公式(移动端要完美显示，不要出现公式显示不全和超出屏幕的情况，切记不要忘记转义符号)。
            *   **严格**: 此格式为固定要求，**不受用户自定义规则影响**，必须严格遵守。
        *   **标点符号正确使用**: 标点符号一定要正确使用，不要出现例如 空格代替逗号句号等错误。
        *   **错别字纠正**: 若发现资料中存在错别字请一定要给予纠正（因为大部分资料都是文字识别的，错别字率较高）。
        ## 3. 特殊内容处理规则
        *   **源发题目优先**: 若输入资料中已包含现成题目：
            *   直接采纳原题目和答案（若有）。
            *   若无答案，请一定要解答题目提供答案。
            *   确保输出的题型与原文一致。
            *   此情况下，可**忽略**其他制卡规则（如原子化、句式调整等），以保留题目原貌。
        *   **非学习资料处理**: 若输入资料为非典型学习材料（如图片、普通文章、描述性文本等）：
            *   主动提取其中蕴含的、可转化为卡片学习的知识点或信息进行制卡。
            *   由于处理的是每页pdf/书籍的图片及大文本截断处理，存在起始末尾内容截断情况，这部分内容请你进行合理补全。
            *   若检测到部分或全部内容为封面、目录、扉页则不进行制卡。
        ## 4. 语言与表达优化 (简体中文环境)
        ### 4.1. 文本精炼
        *   **去冗除赘**: 删除不必要的副词、重复词（例："可能实际上有效" → "可能有效"）。
        *   **明确主干**: 突出句子的主谓宾结构，使语义清晰，补充必要信息使语义完整（例："因天气取消" → "恶劣天气致活动取消"）。
        ### 4.2. 句式调整
        *   **拆分长句**: 超过30个汉字的句子应切分为更短、更易理解的短句。
        *   **主动语态**: 尽量将被动句式（例："报告被提交"）转换为主动句式（例："提交报告"）。
        ### 4.3. 词汇优化
        *   **通俗易懂**: 将复杂、生僻的词汇替换为更通俗、易于理解的表达（例："异质性" → "差异"）。
        *   **合并同义**: 整合意义相近或重复的表述（例："80%的人表示满意，满意度高达八成" → "80%满意"）。
        *   **上位词概括**: 适当使用上位词（父类词）归纳具体实例，使表达更简洁（例："笔记本电脑、手机、平板" → "电子设备"；"柳树、杨树、松树" → "树木"）。
        *   **核心词保护 (极其重要)**: **严禁**替换任何专有名词、专业术语、核心概念或考试相关的关键词。任何可能影响理解准确性或考试得分的词汇都必须保留原样。
        ### 4.4. 逻辑重组
        *   **有序呈现**: 根据时间顺序或因果关系调整信息顺序，使其更符合逻辑。
        *   **结论先行**: 对于包含因果关系的描述，优先或直接阐述结论（例："因为销售额出现了显著的下降，所以我们决定对现有的市场策略进行调整" → "销售下降故调整策略"）。\n\n
        """
            
            prompt = PromptTemplate(
                template="{prompt}\n\n制卡资料: \n\n{data}\n\n输出要求：\n\n{format_instructions}",
                input_variables=["data"],
                partial_variables={
                    "prompt": custom_prompt,
                    "format_instructions": parser.get_format_instructions()
                },
            )
            
            # 处理文件
            file_path = self.form_data.get("selected_file_path", "")
            total_cards = 0
            
            # 判断 file_path 是否为数组，如果是，则将图片列表转为 PDF 并保存到临时文件
            if isinstance(file_path, list):
                self.progress.emit("转换图片为PDF...")
                from PIL import Image
                import tempfile

                images = []
                for img_path in file_path:
                    img = Image.open(img_path)
                    if img.mode != "RGB":
                        img = img.convert("RGB")
                    images.append(img)
                if images:
                    temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
                    images[0].save(temp_pdf.name, save_all=True, append_images=images[1:])
                    temp_pdf.close() 
                    file_path = temp_pdf.name
            
            if file_path and file_path.lower().endswith((".md", ".txt")):
                self.progress.emit("读取文本文件...")
                with open(file_path, 'r', encoding='utf-8') as f:
                    self.form_data["input_text"] = f.read()
            
            if file_path and file_path.lower().endswith(".pdf"):
                self.progress.emit("处理PDF文件...")
                total_cards += self._process_pdf_file(file_path, prompt, llm, new_parser)
            elif file_path and file_path.lower().endswith((".mp3", "wav", "mp4", "avi")):
                self.progress.emit("处理音频/视频文件...")
                total_cards += self._process_audio_video_file(file_path, prompt, llm, new_parser)
            elif self.form_data.get("input_text"):
                text = self._split_markdown_intelligently(self.form_data.get("input_text"),int(self.form_data.get("character_truncation")))
                for i, text_chunk in enumerate(text, 1):
                    self.progress.emit(f"正在处理第 {i}/{len(text)} 块: {text_chunk[:15]}...")
                    chain = prompt | llm | new_parser
                    card_list = chain.invoke({"data": text_chunk})
                    if card_list:
                        self.ai_interface.add_card_to_anki(card_list, self.form_data.get("deck_name"), self.form_data.get("template_name"))
                        total_cards += len(card_list.cards)
            else:
                self.error.emit("请选择文件或输入文本内容")
                return
                
            self.success.emit(f"任务完成！共生成 {total_cards} 张卡片")
            
        except Exception as e:
            import traceback
            error_stack = traceback.format_exc()
            self.error.emit(f"处理过程中出现错误: {str(e)}\n\n堆栈信息:\n{error_stack}")
    
    def _process_pdf_file(self, file_path, prompt, llm, new_parser):
        """处理PDF文件"""
        # 解析页码选择为数组
        page_selection = self.form_data.get("page_selection", "")
        selected_pages = []
        
        if page_selection.strip():
            parts = page_selection.split(',')
            for part in parts:
                part = part.strip()
                if '-' in part:
                    try:
                        start, end = map(int, part.split('-'))
                        selected_pages.extend(range(start, end + 1))
                    except ValueError:
                        pass
                else:
                    try:
                        selected_pages.append(int(part))
                    except ValueError:
                        pass
        
        import fitz  # PyMuPDF
        doc = fitz.open(file_path)
        
        if not selected_pages:
            selected_pages = list(range(1, doc.page_count + 1))
        
        text = ""
        total_cards = 0
        
        ocr_llm = ChatOpenAI(
            model="gemini-2.5-flash-preview-05-20",
            api_key=cfg.get(cfg.llmApiKey),
            base_url=BASE_URL
        )
        
        for i, page_num in enumerate(selected_pages):
            if self.is_cancelled:
                break
                
            self.progress.emit(f"处理第 {page_num} 页 ({i+1}/{len(selected_pages)})")
            
            if 1 <= page_num <= doc.page_count:
                page = doc.load_page(page_num-1)
                annotations = page.annots()
                rect_annotations = [a for a in annotations if a.type[0] == 4]
                
                if rect_annotations:
                    for annot in rect_annotations:
                        rect = annot.rect
                        pix = page.get_pixmap(clip=rect)
                        img_base64 = self.ai_interface._pixmap_to_base64(pix)
                        message = self.ai_interface._build_ocr_message(img_base64)
                        response = ocr_llm.invoke([message])
                        text += response.content + "\n\n"
                        
                        if len(text) > int(self.form_data.get("character_truncation")):
                            chain = prompt | llm | new_parser
                            card_list = chain.invoke({"data": text})
                            if card_list:
                                self.ai_interface.add_card_to_anki(card_list, self.form_data.get("deck_name"), self.form_data.get("template_name"))
                                total_cards += len(card_list.cards)
                            text = ""
                else:
                    pix = page.get_pixmap()
                    img_base64 = self.ai_interface._pixmap_to_base64(pix)
                    message = self.ai_interface._build_ocr_message(img_base64)
                    response = ocr_llm.invoke([message])
                    text += response.content + "\n\n"
                    
                    if len(text) > int(self.form_data.get("character_truncation")):
                        chain = prompt | llm | new_parser
                        card_list = chain.invoke({"data": text})
                        if card_list:
                            self.ai_interface.add_card_to_anki(card_list, self.form_data.get("deck_name"), self.form_data.get("template_name"))
                            total_cards += len(card_list.cards)
                        text = ""
        
        if text:
            chain = prompt | llm | new_parser
            card_list = chain.invoke({"data": text})
            if card_list:
                self.ai_interface.add_card_to_anki(card_list, self.form_data.get("deck_name"), self.form_data.get("template_name"))
                total_cards += len(card_list.cards)
        
        doc.close()
        return total_cards
    def _split_markdown_intelligently(
        self,
        text_content: str,
        max_length: int = 5000,
        min_length_leeway: int = 500  # How far back from max_length we are willing to search for a good split
    ) -> list[str]:
        """
        Splits a Markdown text into chunks, trying to respect Markdown syntax and sentence structure.

        Args:
            text_content: The Markdown string to split.
            max_length: The desired maximum length for each chunk.
            min_length_leeway: The maximum number of characters to look back from max_length
                            to find a preferred split point. A smaller leeway means splits
                            will be closer to max_length, even if it's a less ideal point.
                            A larger leeway allows for better split points but might result
                            in shorter chunks.

        Returns:
            A list of strings, where each string is a chunk of the original text.
        """
        if not text_content:
            return []

        chunks = []
        current_pos = 0
        text_len = len(text_content)

        if min_length_leeway <= 0: # Ensure leeway is positive
            min_length_leeway = max_length // 10 # Default to 10% of max_length if invalid

        # Order of preference for splitting (delimiters and their lengths)
        # We want to split *after* these delimiters.
        # Regex might be more robust for some, but rfind is simpler here.
        # Tuples: (delimiter_string, length_of_delimiter_to_add_to_split_point)
        preferred_delimiters = [
            ('\n\n', 2),      # 段落分隔（最高优先级）
            ('```\n', 4),     # 代码块结束
            ('\n', 1),        # 普通换行
            ('。', 1),        # 中文句号
            ('！', 1),        # 中文感叹号
            ('？', 1),        # 中文问号
            ('. ', 2),        # 英文句号+空格
            ('! ', 2),        # 英文感叹号+空格
            ('? ', 2),        # 英文问号+空格
            (' ', 1)          # 空格（最低优先级）
        ]

        while current_pos < text_len:
            # Determine the end of the current potential chunk
            end_pos = min(current_pos + max_length, text_len)

            # If the remaining text is shorter than or equal to max_length, it's the last chunk
            if end_pos == text_len:
                chunk = text_content[current_pos:end_pos]
                if chunk.strip(): # Only add if not just whitespace
                    chunks.append(chunk)
                break

            # Search backwards from end_pos for a preferred split point
            actual_split_pos = -1

            # Define the window to search for delimiters
            # Start searching from a bit before end_pos to allow for better semantic breaks
            search_start_limit = max(current_pos, end_pos - min_length_leeway)
            
            # If the chunk from current_pos to search_start_limit is too small,
            # it means our leeway is too large for the current position. Adjust.
            if search_start_limit <= current_pos and (end_pos - current_pos) < (max_length * 0.5): # if the potential chunk is already small
                search_start_limit = current_pos # search the whole small potential chunk

            for delim_str, delim_len in preferred_delimiters:
                # Find the last occurrence of the delimiter within the search window
                # The search window for rfind is [search_start_limit, end_pos)
                found_idx = text_content.rfind(delim_str, search_start_limit, end_pos)

                if found_idx != -1:
                    # Potential split is after the delimiter
                    potential_split_point = found_idx + delim_len
                    # Ensure we are making progress and not creating an empty chunk due to split at current_pos
                    if potential_split_point > current_pos:
                        actual_split_pos = potential_split_point
                        break  # Found the highest-priority delimiter in range

            if actual_split_pos != -1:
                # Found a preferred delimiter
                chunk = text_content[current_pos:actual_split_pos]
                current_pos = actual_split_pos
            else:
                # No preferred delimiter found in the leeway window, or split point was not advancing.
                # Perform a hard cut at end_pos.
                # This might happen if there's a very long token (e.g., URL, base64 string)
                # or if all delimiters are before search_start_limit.
                chunk = text_content[current_pos:end_pos]
                current_pos = end_pos
            
            if chunk.strip(): # Only add if not just whitespace
                chunks.append(chunk)
                
        # Sanity check: remove any completely empty strings if they somehow got added
        return [c for c in chunks if c.strip()]
    def _process_audio_video_file(self, file_path, prompt, llm, new_parser):
        """处理音频和视频文件"""
        import tempfile
        from moviepy.video.io.VideoFileClip import VideoFileClip
        import os
        
        temp_audio_path = None
        try:
            if file_path.lower().endswith((".mp4", ".avi")):
                temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
                temp_audio_path = temp_audio.name
                temp_audio.close()
                
                video = VideoFileClip(file_path)
                video.audio.write_audiofile(temp_audio_path)
                file_path = temp_audio_path
                video.close()
            
            files = {
                'file': (os.path.basename(file_path), open(file_path, 'rb'), f'audio/{file_path.split(".")[-1]}')
            }
            
            headers = {
                'Authorization': f'Bearer {cfg.get(cfg.llmApiKey)}'
            }
            data = {
                'model': 'whisper-1',
                'response_format': 'json',
                'language': 'zh',
                'prompt': '简体中文'
            }
            
            transcription_response = requests.post(BASE_URL + "/audio/transcriptions", files=files, data=data, headers=headers)
            files['file'][1].close()
            
            if transcription_response.status_code == 200:
                transcription_result = transcription_response.json()
                transcribed_text = transcription_result.get("text", "")
                
                if transcribed_text:
                    chain = prompt | llm | new_parser
                    card_list = chain.invoke({"data": transcribed_text})
                    if card_list:
                        self.ai_interface.add_card_to_anki(card_list, self.form_data.get("deck_name"), self.form_data.get("template_name"))
                        return len(card_list.cards)
            else:
                self.error.emit(f"音频转录失败: {transcription_response.status_code}")
                
        finally:
            if temp_audio_path and os.path.exists(temp_audio_path):
                os.unlink(temp_audio_path)
        
        return 0

class AICardInterface(QWidget):
    """ AI制卡界面 """
    
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setObjectName("aiCard")
        self.selected_file_path: str | None = None # 新增：存储选择的文件路径
        self.worker = None  # 工作线程引用
        
        self.vBoxLayout = QVBoxLayout(self)
        self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
        self.vBoxLayout.setSpacing(0)
        
        # 使用滚动区域作为主要容器
        self.scrollArea = ScrollArea(self)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        
        # 创建滚动区域内的内容控件
        self.scrollWidget = QWidget()
        self.scrollWidget.setObjectName("scrollWidget")
        self.scrollLayout = QVBoxLayout(self.scrollWidget) # 主内容布局，Pivot 和 StackedWidget 将添加到这里
        self.scrollLayout.setContentsMargins(10, 10, 10, 10) # 给内容区域一些边距
        self.scrollLayout.setSpacing(10)

        # Pivot 和 QStackedWidget
        self.pivot = Pivot(self)
        self.stackedWidget = QStackedWidget(self)

        # 创建页面
        self.selectFilePage = QWidget(self)
        selectFileLayout = QVBoxLayout(self.selectFilePage)
        selectFileLayout.setContentsMargins(0,0,0,0) # 子页面布局通常不需要额外边距，除非有特殊需求
        self.dropSingleFileWidget = CustomDropSingleFileWidget(self.selectFilePage)
        self.dropSingleFileWidget.setFixedHeight(200)
        self.dropSingleFileWidget.setSuffixes([
            "pdf", "txt", "md","mp3","wav","mp4","avi"
        ])
        selectFileLayout.addWidget(self.dropSingleFileWidget) # 移除居中对齐，使其宽度占满
        selectFileLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.selectFilePage.setLayout(selectFileLayout)
        # 连接单文件选择信号
        self.dropSingleFileWidget.fileDropped.connect(self._onSingleFileSelected)
        # 连接清空文件信号
        self.dropSingleFileWidget.fileCleared.connect(self._onSingleFileCleared)

        # --- 2. 选择图片页面 ---
        self.selectImagePage = QWidget(self)
        selectImageLayout = QVBoxLayout(self.selectImagePage)
        selectImageLayout.setContentsMargins(0,0,0,0)
        self.dropMultiFilesWidget = CustomDropMultiFilesWidget(self.selectImagePage)
        self.dropMultiFilesWidget.setFixedHeight(200)
        self.dropMultiFilesWidget.setSuffixes(["jpg", "jpeg", "png"]) # 注意：setSuffixes 通常不需要前导点
        selectImageLayout.addWidget(self.dropMultiFilesWidget) # 移除居中对齐，使其宽度占满
        selectImageLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.selectImagePage.setLayout(selectImageLayout)
        # 连接多文件选择信号
        self.dropMultiFilesWidget.filesDropped.connect(self._onMultiFilesSelected)
        # 连接清空文件信号
        self.dropMultiFilesWidget.filesCleared.connect(self._onMultiFilesCleared)
        
        # --- 3. 输入文本页面 ---
        self.inputTextPage = QWidget(self)
        inputTextLayout = QVBoxLayout(self.inputTextPage)
        inputTextLayout.setContentsMargins(0,0,0,0)
        self.textEdit = TextEdit(self.inputTextPage) # 使用 TextEdit 而不是 QWidget
        self.textEdit.setFixedHeight(200)
        self.textEdit.setPlaceholderText("在此输入或粘贴文本...")
        inputTextLayout.addWidget(self.textEdit)
        inputTextLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.inputTextPage.setLayout(inputTextLayout)
        
        # 添加子界面到 Pivot 和 StackedWidget
        # 参数: widget, objectName, text, icon
        self._addSubInterface(self.selectFilePage, 'selectFilePage', '选择文件', FIF.DOCUMENT)
        self._addSubInterface(self.selectImagePage, 'selectImagePage', '选择图片', FIF.PHOTO)
        self._addSubInterface(self.inputTextPage, 'inputTextPage', '输入文本', FIF.EDIT)

        # 将 Pivot 和 StackedWidget 添加到滚动区域的布局中
        self.scrollLayout.addWidget(self.pivot)
        self.scrollLayout.addWidget(self.stackedWidget)
        
        # 设置默认选中的项目
        self.pivot.setCurrentItem('selectFilePage')
        # 连接 Pivot 的选项变化信号到 StackedWidget 的切换槽
        self.pivot.currentItemChanged.connect(self._onCurrentItemChanged)
        
        # 设置滚动区域的内容控件
        self.scrollArea.setWidget(self.scrollWidget)
        self.vBoxLayout.addWidget(self.scrollArea)
        
        # --- 基础设置 --- 
        self.basicSettingsGroup = SettingCardGroup("基础设置", self.scrollWidget) # 将父控件改为 self.scrollWidget
        self.pageSelectCard = LineEditSettingCard(
            FIF.EDIT, 
            "页码选择", 
            "空为全部;输入1,3提取1、3页;输入5-9提取5-9页。", 
            parent=self.basicSettingsGroup
        )
        self.pageSelectCard.lineEdit.setPlaceholderText("例如: 1,3,5-9")
        self.pageSelectCard.lineEdit.textChanged.connect(self._on_page_select_text_changed)
        self.basicSettingsGroup.addSettingCard(self.pageSelectCard)

        # 字符截断设置
        self.characterTruncationSlider = SliderSettingCard(
            FIF.CUT, 
            "字符截断",
            "每多少字符一次AI请求（避免一次处理过多内容导致内容遗漏）",
            cfg.characterTruncation, # 使用配置项
            parent=self.basicSettingsGroup,
            minimum=1000,
            maximum=10000
        )
        self.basicSettingsGroup.addSettingCard(self.characterTruncationSlider)

        # 卡片数量设置
        self.cardCountSlider = SliderSettingCard(
            FIF.TAG, # 或者选择一个更合适的图标
            "卡片数量",
            "每截断字符生成卡片数量(0为完整覆盖所有知识点)",
            cfg.aiCardCount, # 使用配置项
            parent=self.basicSettingsGroup,
            minimum=0,
            maximum=100
        )
        self.basicSettingsGroup.addSettingCard(self.cardCountSlider)

        self.scrollLayout.addWidget(self.basicSettingsGroup) # 将设置组添加到滚动区域的布局中

        # --- 自定义提示词设置 ---
        self.customPromptGroup = SettingCardGroup("提示词", self.scrollWidget)
        self.customPromptTextEdit = TextEdit(self.customPromptGroup) # 使用 TextEdit
        self.customPromptTextEdit.setFixedHeight(100)
        self.customPromptTextEdit.setPlaceholderText("请输入你的制卡需求（可为空）")
        self.customPromptTextEdit.setText(cfg.get(cfg.customPromptText)) # 加载保存的文本
        self.customPromptTextEdit.textChanged.connect(self._on_custom_prompt_text_changed) # 连接信号
        self.customPromptGroup.addSettingCard(self.customPromptTextEdit) # 使用 addSettingCard
        self.scrollLayout.addWidget(self.customPromptGroup)

        # --- 开始制卡设置 ---
        self.startMakingCardGroup = SettingCardGroup("开始制卡", self.scrollWidget)
        
        self.deckNameCard = LineEditSettingCard(
            FIF.LABEL, "卡组名称", "请输入卡组名称",
            configItem=cfg.deckName, # 值不需要保存
            parent=self.startMakingCardGroup
        )
        self.startMakingCardGroup.addSettingCard(self.deckNameCard)

        self.templateNameCard = LineEditSettingCard(
            FIF.DOCUMENT, "模板名称", "打开Anki->工具->管理笔记模板->选择所需模板->字段->编辑每个字段的描述",
            configItem=cfg.templateName,
            parent=self.startMakingCardGroup
        )
        self.startMakingCardGroup.addSettingCard(self.templateNameCard)

        self.submitTaskCard = PrimaryPushSettingCard(
            "开始制卡", FIF.SAVE, "开始制卡", "请耐心等待制卡完成（需持续数分钟至数小时）", self.startMakingCardGroup
        )
        self.startMakingCardGroup.addSettingCard(self.submitTaskCard)

        self.scrollLayout.addWidget(self.startMakingCardGroup)

        self.scrollLayout.addStretch(1) # 添加伸缩因子，将前面的内容顶上去
        
        # 连接信号
        self.textEdit.textChanged.connect(self._update_button_states)
        self.deckNameCard.lineEdit.textChanged.connect(self._update_button_states)
        self.templateNameCard.lineEdit.textChanged.connect(self._update_button_states)
        self.submitTaskCard.clicked.connect(self._on_submit_task_clicked)
        
        # 应用样式表
        StyleSheet.AI_CARD_INTERFACE.apply(self)

        # 初始化按钮状态
        self._update_button_states()

    def _addSubInterface(self, widget: QWidget, object_name: str, text: str, icon):
        widget.setObjectName(object_name)
        # widget.setAlignment(Qt.AlignmentFlag.AlignCenter) # QWidget没有setAlignment, 如果需要内容居中，需要在widget内部的布局或控件上设置
        self.stackedWidget.addWidget(widget)
        self.pivot.addItem(
            routeKey=object_name,
            text=text,
            # onClick=lambda: self.stackedWidget.setCurrentWidget(widget), # currentItemChanged 信号会处理切换
            icon=icon
        )

    def _onCurrentItemChanged(self, routeKey: str):
        # 这个槽函数用于响应 Pivot.currentItemChanged(str) 信号
        # 在较新版本的 qfluentwidgets 中，Pivot.addItem 时可以直接关联切换逻辑，
        # 或者通过 objectName 查找 widget
        widget = self.findChild(QWidget, routeKey)
        if widget:
            self.stackedWidget.setCurrentWidget(widget)

    def _onSingleFileSelected(self, file_path: str):
        """ 单个文件选择成功时的处理 """
        self.dropSingleFileWidget.setTitle(file_path)
        self.selected_file_path = file_path
        self._update_button_states()

    def _onSingleFileCleared(self):
        """ 清空单个文件时的处理 """
        self.selected_file_path = None
        self._update_button_states()

    def _onMultiFilesSelected(self, file_paths: list):
        """ 多个文件选择成功时的处理 """
        if file_paths:
            self.selected_file_path=file_paths
            #  可以只显示第一个文件名或者文件数量
            self.dropMultiFilesWidget.setTitle(f"{file_paths[0]} 等{len(file_paths)} 个文件")
            self._update_button_states()  # 添加这行来更新按钮状态

    def _onMultiFilesCleared(self):
        """ 清空多个文件时的处理 """
        self.selected_file_path = None
        self._update_button_states()

    def _validatePageRange(self, text: str, show_infobar: bool = True) -> bool:
        """ 验证页码范围输入 """
        valid_format = True # 先假设格式是正确的

        if not text.strip():
            # 空字符串是有效格式，表示处理所有页面
            pass # valid_format 保持 True
        else:
            parts = text.split(',')
            for part_idx, part_str in enumerate(parts):
                part = part_str.strip()
                if not part:
                    # 处理像 "1,,2" 这样的情况，单个空部分不视为错误，但如果整个字符串是 "," 则视为空
                    if len(parts) == 1: # 如果整个输入只是一个或多个逗号
                        if show_infobar: InfoBar.error("错误", "页码输入不能为空", duration=3000, parent=self)
                        valid_format = False
                        break
                    continue

                if '-' in part:
                    try:
                        start, end = map(int, part.split('-'))
                        if start < 1 or end < 1 or start > end:
                            if show_infobar: InfoBar.error("错误", f"无效的页码范围: {part}", duration=3000, parent=self)
                            valid_format = False
                            break
                    except ValueError:
                        if show_infobar: InfoBar.error("错误", f"无效的页码范围格式: {part}", duration=3000, parent=self)
                        valid_format = False
                        break
                else:
                    try:
                        page = int(part)
                        if page < 1:
                            if show_infobar: InfoBar.error("错误", f"无效的页码: {part}", duration=3000, parent=self)
                            valid_format = False
                            break
                    except ValueError:
                        if show_infobar: InfoBar.error("错误", f"无效的页码格式: {part}", duration=3000, parent=self)
                        valid_format = False
                        break
        
        return valid_format

    def _on_page_select_text_changed(self, text: str):
        """ 页码输入框内容改变时的处理 """
        self._validatePageRange(text, show_infobar=True) # 输入时验证并提示
        self._update_button_states()

    def _update_button_states(self):
        """ 更新导出和同步按钮的启用状态 """
        # 如果正在处理任务，禁用按钮
        if self.worker and self.worker.isRunning():
            self.submitTaskCard.setEnabled(False)
            return
            
        is_page_range_format_valid = self._validatePageRange(self.pageSelectCard.lineEdit.text(), show_infobar=False)

        has_file_input = bool(self.selected_file_path)
        has_text_input = bool(self.textEdit.toPlainText().strip())
        is_input_present = has_file_input or has_text_input

        has_deck_name = bool(self.deckNameCard.lineEdit.text().strip())
        has_template_name = bool(self.templateNameCard.lineEdit.text().strip())

        can_proceed = is_page_range_format_valid and is_input_present and has_deck_name and has_template_name
        
        self.submitTaskCard.setEnabled(can_proceed)

    def collect_form_data(self) -> dict:
        """收集所有表单数据，返回字典，action为点击的按钮类型"""
        data = {
            "selected_file_path": self.selected_file_path,
            "input_text": self.textEdit.toPlainText().strip(),
            "deck_name": self.deckNameCard.lineEdit.text().strip(),
            "template_name": self.templateNameCard.lineEdit.text().strip(),
            "custom_prompt": self.customPromptTextEdit.toPlainText().strip(),
            "character_truncation": cfg.get(cfg.characterTruncation),
            "ai_card_count": cfg.get(cfg.aiCardCount),
            "page_selection": self.pageSelectCard.lineEdit.text().strip()
        }
        return data

    def _pixmap_to_base64(self, pix) -> str:
        """Converts a PyMuPDF pixmap to a base64 encoded PNG string."""
        img_data = pix.tobytes("png")
        img_stream = io.BytesIO(img_data)
        img = Image.open(img_stream)
        buffer = io.BytesIO()
        img.save(buffer, format="PNG")
        img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
        return img_base64
    

    def _build_ocr_message(self, img_base64: str):
        """构建OCR用的HumanMessage对象"""
        return HumanMessage(
            content=[
                {
                    "role": "system",
                    "content": "你是强大的ocr工具"
                },
                {"type": "text", "text": "为了尽可能保留原文格式，请把这张图片完完整整文字识别为html格式不要遗漏任何信息（一定要清楚识别出划线、高亮、加粗部分）"},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{img_base64}"
                    }
                }
            ]
        )

    def _on_submit_task_clicked(self):
        """开始制卡按钮点击处理（现在使用多线程）"""
        # 如果已经有任务在运行，则忽略
        if self.worker and self.worker.isRunning():
            return

        is_page_range_format_valid = self._validatePageRange(self.pageSelectCard.lineEdit.text(), show_infobar=True)
        if not is_page_range_format_valid:
            return

        has_file_input = bool(self.selected_file_path)
        has_text_input = bool(self.textEdit.toPlainText().strip())
        if not (has_file_input or has_text_input):
            InfoBar.error("错误", "请选择文件或输入文本内容", duration=3000, parent=self)
            return

        # 显示确认对话框
        title = "任务提交确认"
        content = "请确保页码、数量、提示词设置正确！\n一旦提交，任务将无法中断或取消！"
        dialog = Dialog(title, content, self.window())
        if not dialog.exec():
            return

        form_data = self.collect_form_data()
        
        # 检查模板名称是否存在
        template_name = form_data.get("template_name", "")
        if template_name:
            try:
                # 使用AnkiConnect API检查模板是否存在
                payload = {
                    "action": "modelNames",
                    "version": 6
                }
                
                response = requests.post(ANKI_URL, json=payload)
                if response.status_code == 200:
                    result = response.json()
                    if result.get("error") is None:
                        model_names = result.get("result", [])
                        if template_name not in model_names:
                            InfoBar.warning(
                                "警告", 
                                f"模板 '{template_name}' 在Anki中不存在，请确认模板名称是否正确。", 
                                duration=5000, 
                                parent=self
                            )
                            return
                    else:
                        InfoBar.warning(
                            "警告", 
                            f"无法验证模板名称: {result.get('error')}", 
                            duration=5000, 
                            parent=self
                        )
                        return
                else:
                    InfoBar.warning(
                        "警告", 
                        "无法连接到Anki，请确保Anki已启动且AnkiConnect插件已安装。", 
                        duration=5000, 
                        parent=self
                    )
                    return
            except Exception as e:
                InfoBar.warning(
                    "警告", 
                    f"无法连接到Anki，请确保Anki已启动且AnkiConnect插件已安装。", 
                    duration=5000, 
                    parent=self
                )
                return
        
        # 创建并启动工作线程
        self.worker = TaskWorker(form_data, self)
        
        # 连接信号
        self.worker.success.connect(self._on_task_success)
        self.worker.error.connect(self._on_task_error)
        self.worker.progress.connect(self._on_task_progress)
        self.worker.finished.connect(self._on_task_finished)
        
        # 更新UI状态
        self.submitTaskCard.setEnabled(False)
        self.submitTaskCard.setContent("任务进行中...")
        
        # 显示开始信息
        InfoBar.info("提示", "任务已开始，请耐心等待...", duration=3000, parent=self)
        
        # 启动线程
        self.worker.start()

    def _on_task_success(self, message: str):
        """任务成功完成"""
        InfoBar.success("成功", message, duration=5000, parent=self)

    def _on_task_error(self, error_message: str):
        """任务执行出错"""
        InfoBar.error("错误", error_message, duration=5000, parent=self)

    def _on_task_progress(self, progress_message: str):
        """任务进度更新"""
        self.submitTaskCard.setContent(progress_message)

    def _on_task_finished(self):
        """任务结束（无论成功还是失败）"""
        self.submitTaskCard.setContent("请耐心等待制卡完成")
        self._update_button_states()
        self.worker = None

    def add_card_to_anki(self, card_list, deck_name, model_name):
        """添加卡片到Anki"""
        try:
            # 检查卡组是否存在
            check_deck_response = requests.post(ANKI_URL, json={
                "action": "deckNames",
                "version": 6
            })
            
            if check_deck_response.status_code == 200:
                deck_result = check_deck_response.json()
                if deck_result.get("error") is None:
                    existing_decks = deck_result.get("result", [])
                    
                    # 如果卡组不存在，则创建
                    if deck_name not in existing_decks:
                        requests.post(ANKI_URL, json={
                            "action": "createDeck",
                            "version": 6,
                            "params": {
                                "deck": deck_name
                            }
                        })
                        
                else:
                    print(f"检查卡组失败: {deck_result.get('error')}")
        except Exception as e:
            print(f"检查/创建卡组时出错: {str(e)}")

        if card_list:
            try:
                # 使用AnkiConnect添加笔记
                for card in card_list.cards:
                    # 准备笔记数据
                    note = {
                        "deckName": deck_name,
                        "modelName": model_name,
                        "fields": {}
                    }
                    
                    # 遍历卡片的所有字段，将其添加到Anki笔记中
                    for field_name, field_value in card:
                        if field_name and field_value:
                            note["fields"][field_name] = field_value
                    
                    # 发送请求到AnkiConnect
                    response = requests.post(ANKI_URL, json={
                        "action": "addNote",
                        "version": 6,
                        "params": {
                            "note": note
                        }
                    })
                    
                    result = response.json()
                    if result.get("error"):
                        print(f"添加笔记失败: {result.get('error')}")
                
            except Exception as e:
                print(f"添加卡片到Anki失败: {str(e)}")

    def _on_custom_prompt_text_changed(self):
        """ 自定义提示词内容改变时的处理 """
        cfg.set(cfg.customPromptText, self.customPromptTextEdit.toPlainText())
